home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Floppyshop 2
/
Floppyshop - 2.zip
/
Floppyshop - 2.iso
/
art&graf.ix
/
art-4542
/
rshade30
/
primitiv.e_m
< prev
next >
Wrap
Text File
|
1993-09-06
|
22KB
|
664 lines
.\"
.\" Brief tutorial on adding new primitives to rayshade.
.\" Craig Kolb 10/89
.\"
.\" $Id: primitive.ms,v 3.0.1.1 90/04/10 17:22:36 craig Exp $
.\"
.\" $Log: primitive.ms,v $
.\" Revision 3.0.1.1 90/04/10 17:22:36 craig
.\" patch5: addscaledvec() no longer returns a structure...
.\"
.\" Revision 3.0 89/10/24 18:07:30 craig
.\" Baseline for first official release.
.\"
.de D(
.DS
.nr PS 8
.ps 8
.nr VS 12p
.vs 12p
.cs 1 20
..
.de D)
.DE
.nr PS 10
.ps 10
.nr VS 18p
.vs 18p
.cs 1
..
.DS
.ND October, 1989
.RP
.de ]H
.nr PS 10
.ps 10
'sp |0.5i-1
.tl 'Rayshade Primitive Tutorial'-%-'Draft'
.ps
.ft
'sp |1.0i
.ns
..
.wh0 ]H
.pn 2
.LP
.TL
Adding Primitives to Rayshade
.TL
\fR\fIDraft\fR
.AU
Craig Kolb
.AI
Department of Mathematics
Yale University
New Haven, CT 06520
.sp .5i
.nr VS 18pts
.PP
This tutorial describes the process of adding new primitives to
rayshade. Although the hooks for adding primitives are relatively
straight-forward, it is difficult to see the "big picture" by
studying the source code. While this tutorial is primarily
meant for those interested getting their hands dirty, it
provides a overview of the design of at least one portion
of rayshade and thus may be of interest even if you are not
planning on making modifications.
.LP
Adding a new primitive involves modifying at least six source
files and creating at least one new file:
.NH
primobj.h
.LP
A datatype for the new primitive is added, and a pointer for
the new type is added to the Primitive type.
.NH
constants.h
.LP
A numerical type is reserved for the primitive.
.NH
intersect.c
.LP
Various arrays of pointers to functions are modified to include
new functions which will be written for the new primitive.
The name of the new primitive is added to a list of names,
and an array of flags is modified to reflect the addition of
the new primitive.
.NH
funcdefs.h
.LP
The ray-primitive intersection function, primitive creation
function, normal calculation function, and bounding-box
calculation function are declared.
.NH
<primitive-type>.c
.LP
This file contains
all of the code needed to perform
ray-primitive intersection tests, find normals, and compute
bounding boxes for the new primitive type.
.NH
input_lex.l
.LP
The keyword used to identify the new primitive is added to
the list of recognized keywords.
.NH
input_yacc.y
.LP
The new primitive is added to the list of primitives recognized by
the yacc grammar. The added code will call the routine
which creates and returns a reference to the primitive
which was defined in <primitive-type>.c
.LP
In addition, the Makefile must be updated to reflect the existence of the
new source file.
.sp 1
.LP
In this tutorial, a new primitive
.I disc,
is added to rayshade. A disc is specified by its center, radius,
and normal.
.br
.NR PS 12
.ps 12
.sp .5
\fBThe Primitive type\fR
.nr PS 10
.ps 10
.sp .5
.LP
All Primitives in rayshade are referenced using a single Primitive structure.
This structure is defined as:
.D(
typedef struct Primitive {
char type; /* object type */
struct Surface *surf; /* default surface */
union {
Sphere *p_sphere;
Box *p_box;
Triangle *p_triangle;
Superq *p_superq;
Plane *p_plane;
Cylinder *p_cylinder;
Polygon *p_poly;
Cone *p_cone;
Hf *p_hf;
} objpnt; /* Pointer to primitive */
} Primitive;
.D)
.LP
The
.I type
field is used by various routines in intersect.c to determine
which elements in a number of arrays should be used when dealing with
a given Primitive structure.
The
.I surf
field points to the surface associated with the primitive.
The
.I objpnt
field is a union of pointers to different primitive types.
The
.I type
of the Primitive is used to determine which pointer to
dereference.
.NH 0
Modifying primobj.h
.LP
Primobj contains structures describing each primitive type. For example,
a sphere is defined as:
.D(
typedef struct {
double r; /* radius */
double x, y, z; /* position */
} Sphere;
.D)
We must define a similar structure for the disc primitive. After the
definition of the Hf type, we add:
.D(
/*
* Disc
*/
typedef struct {
Vector center, /* Center of the disc */
normal; /* Normal to disc */
double d, /* Plane constant */
radius; /* Radius of disc */
} Disc;
.D)
.LP
We must also add a pointer for the Disc type to the Primitive type.
So, we add a line to the \fIobjpnt\fR union in the Primitive definition,
giving us:
.D(
typedef struct Primitive {
char type; /* object type */
struct Surface *surf; /* default surface */
union {
Sphere *p_sphere;
Box *p_box;
Triangle *p_triangle;
Superq *p_superq;
Plane *p_plane;
Cylinder *p_cylinder;
Polygon *p_poly;
Cone *p_cone;
Hf *p_hf;
Disc *p_disc;
} objpnt; /* Pointer to primitive */
} Primitive;
.D)
.NH
Modifying constants.h
.LP
In constants.h, there are a series of lines resemblin:
.D(
#define SPHERE 0
#define BOX 1
#define TRIANGLE 2
#define SUPERQ 3
#define PLANE 4
#define CYL 5
#define POLY 6
#define PHONGTRI 7
#define CONE 8
#define HF 9
#define LIST 10
#define GRID 11
.D)
.LP
These lines define the values assigned to the 'type' field in each
Primitive and Object. (Actually, a Primitive can never be of type
LIST or GRID, but an Object may be.) We must add a similar line for
the new primitive.
.LP
When adding new values, we
.I must
add the primitive
.I before
the
lines defining the types for LIST and GRID. This is due to the way
arrays are indexed in intersect.c So, below the HF type, we add a line
for the Disc type and increment the LIST and GRID types:
.D(
#define CONE 8
#define HF 9
#define DISC 10
#define LIST 11
#define GRID 12
.D)
We must also increment PRIMTYPES, the total number of primitives types:
.D(
#define PRIMTYPES 11
.D)
.NH
Modifying intersect.c
.LP
In intersect.c, several arrays are declared and initialized which are indexed
by Primitive or Object type. We must modify these array declarations
to reflect the addition of the new primitive type. The first array to
be modified is :
.D(
double (*objint[])() = {intsph, intbox, inttri, intsup, intplane, intcyl,
intpoly, inttri, intcone, inthf};
.D)
This array of pointers to functions contains the names of the functions
which perform ray/primitive intersection tests. Here,
.I intsph,
the
0th element in the array, is the name of the intersection routine
for the Sphere primitive, which is declared as type 0 in constants.h.
Similarly,
.I intplane
is the 4th element in the array, and the Plane
primitive is defined to be type 4. This is due to the fact that a
Primitive's type field is used as an index into this array.
.LP
So, we must add the name of the new ray/disc intersection test, which
we will name "intdisc", to the objint[] array:
.D(
double (*objint[])() = {intsph, intbox, inttri, intsup, intplane, intcyl,
intpoly, inttri, intcone, inthf, intdisc};
.D)
Similarly, we must modify the objnrm[] and objextent[] arrays to contain
the names of the functions that compute a specific disc's normal and
bounding box:
.D(
int (*objnrm[])() = {nrmsph, nrmbox, nrmtri, nrmsup, nrmplane, nrmcyl,
nrmpoly, nrmtri, nrmcone, nrmhf, nrmdisc};
int (*objextent[])() = {sphextent, boxextent, triextent, supextent,
planeextent, cylextent, polyextent, triextent,
coneextent, hfextent, discextent};
.D)
.LP
In addition, there is an array of Primitive names which is used when
printing statistics. Again, we need to modify the array to include
the name of the Disc primitive:
.D(
char *primnames[PRIMTYPES] = { "Sphere", "Box", "Triangle", "Superq", "Plane",
"Cylinder", "Polygon", "Phongtri", "Cone",
"Heightfield", "Disc"};
.D)
.LP
Lastly, there is an array of flags named CheckBounds[] which is indexed
by Object type. This array is used by intersect() to determine if
a ray/bounding-box intersection test should be performed before a ray/Object
intersection test. If the element in CheckBounds[] corresponding to
an Object's type is TRUE, a ray/Object intersection test will only occur
if a ray/bounding-box intersection test succeeds. (Even if ray/bounding-box
tests are not performed for a given primitive, the bounding box is still
used to determine the extent of the object when it is included in compound
Objects.)
.LP
We will set the flag corresponding to the Disc primitive to be TRUE. Note
that the CheckBounds array is indexed by \fIObject\fR type, so we add the
entry for the Disc type before the final two entries in the array:
.D(
char CheckBounds[] = {TRUE, FALSE, TRUE, FALSE, FALSE, TRUE, TRUE, TRUE,
TRUE, FALSE, TRUE, FALSE, FALSE};
.D)
.NH
Modifying funcdefs.h
.LP
The file funcdefs.h contains a number of function declarations. We must add
declarations for the four new functions to be written. Firstly,
we add the declaration of "nrmdisc" to the normal-finding functions:
.D(
int nrmsph(), nrmbox(), nrmtri(), nrmsup(),nrmplane(), nrmcyl(),
nrmpoly(), nrmcone(), nrmhf(), nrmdisc();
.D)
.LP
Next, we declare "intdisc" with the other ray/primitive intersection
test functions:
.D(
/*
* Intersection routines
*/
double intsph(), intbox(), inttri(), intsup(),intplane(), crossp(), intcyl(),
intpoly(), intcone(), inthf(), intdisc();
.D)
And the bounding-box routine:
.D(
/*
* Extent-box finding routines
*/
int sphextent(),boxextent(),triextent(),supextent(),planeextent(),
cylextent(), polyextent(), coneextent(), hfextent(),
discextent();
.D)
.LP
And lastly, we add "makdisc", the routine which will create a reference
to a particular disc, to the list of object creation functions:
.D(
/*
* Object creation routines
*/
Object *maksph(), *makbox(), *maktri(), *maksup(), *makplane(), *makcyl(),
*makpoly(), *makcone(), *makhf(), *makdisc(), *new_object()
.D)
.NH
Writing disc.c
.LP
And now we must write the four functions makdisc(), intdisc(), nrmdisc()
and discextent(). The first thing you should do is copy
the Copyright
template to the top of disc.c.
.LP
Next, we need to #include the necessary header files. In addition to math.h
and stdio.h, you will need "constants.h" (which includes the definition
of DISC), "typedefs.h" (which includes all sorts of useful structure
definitions), and "funcdefs.h" (which includes all sorts of useful
function declarations).
.NH 2
makdisc()
.LP
Every primitive-creation function must take a least one argument -- the
name of the surface to be associated with the primitive. Besides this
argument, these functions will be passed any parameters needed to define
a particular primitive. For us, this means two vectors (the center of
the disc and its normal) and one double (the radius of the disc).
.LP
The makdisc() routine will do several things. In addition to creating a new
Disc, it must allocate a Primitive structure and set its fields
appropriately -- it must point to the new Disc, have its "type" field
set correctly, and it must point to the surface associated with the
primitive. In addition, an Object which points to this new Primitive
must be created and initialized properly. It is this Object structure
which is returned by makdisc().
.LP
So, we write:
.D(
Object *
makdisc(surf, center, radius, norm)
char *surf;
Vector center, norm;
double radius;
{
Disc *disc; /* Pointer to new disc. */
Primitive *prim; /* Pointer to new Primitive */
Object *newobj; /* Pointer to new Object */
Vector tmpnorm; /* normalized normal */
extern int Quiet; /* True if we shouldn't complain */
extern int yylineno; /* Current line # in input file. */
if (radius < EPSILON) {
if (!Quiet)
fprintf(stderr,"Degenerate disc (line %d).\\n",
yylineno);
/*
* Don't create this primitive.
*/
return (Object *)0;
}
tmpnorm = norm;
if (normalize(&tmpnorm) == 0.) {
if (!Quiet)
fprintf(stderr,"Degenerate disc normal (line %d).\\n",
yylineno);
return (Object *)0;
}
/*
* Allocate new Disc.
*/
disc = (Disc *)Malloc(sizeof(Disc));
/*
* Initialize new disc.
* We store the square of the radius to save us a sqrt().
*/
disc->radius = radius*radius;
disc->center = center;
disc->normal = tmpnorm;
/*
* Compute plane constant.
*/
disc->d = dotp(¢er, &tmpnorm);
/*
* Allocate new Primitive
*/
prim = mallocprim();
/*
* Set Primitive type and pointer to new Disc.
*/
prim->type = DISC;
prim->objpnt.p_disc = disc;
/*
* Search for named surface in list of defined surfaces.
* find_surface() will exit if the surface is not found.
*/
prim->surf = find_surface(surf);
/*
* Create and return new object with NULL name, of type DISC,
* which points to the new Primitive and has no transformation
* associated with it.
*/
return new_object(NULL, DISC, (char *)prim, (Trans *)NULL);
}
.D)
.LP
In this case, our primitive creation function is straight-forward. In
some cases, in order to facilitate ray-primitive intersection tests,
a more general version of the primitive is created, and a transformation
matrix is computed to transform the generic primitive to the specific
primitive requested by the user. Then, one need only perform
intersection tests against the generic version of the primitive. Examples
of these types of primitives are the cone and cylinder. The generic
versions of both of these primitives have their main axes coincident
with the Z axis and their base at the origin. Transformations are
computed in makcyl() and makcone() to transform the generic case to
the specific case. See "cone.c", "cylinder.c" and "input_yacc.y" for
details.
.NH 2
intdisc()
.LP
Each primitive/ray intersection routine is passed three values:
a pointer to a Primitive, the origin of the ray, and the direction of
the ray. Each intersection function must do two things. Firstly,
it must increment an element in the primtests[] array to reflect the
fact that a ray/primitive intersection test has occurred. Most importantly,
each function must return the distance from the ray origin along the
ray direction to the closest point of ray/primitive intersection. This
distance must be greater than EPSILON. If it is less than EPSILON, it is
assumed that no intersection occurs between the ray and the given primitive.
If not valid intersection exists, 0 is returned.
.LP
So, we write:
.D(
double
intdisc(pos, ray, obj)
Vector *pos, *ray;
Primitive *obj;
{
Disc *disc;
Vector hit;
double denom, dist;
extern unsigned long primtests[];
primtests[DISC]++;
disc = obj->objpnt.p_disc;
denom = dotp(&disc->normal, ray);
if (denom == 0.)
return 0.;
dist = (disc->d - dotp(&disc->normal, pos)) / denom;
if (dist > FAR_AWAY || dist < EPSILON)
return 0.;
/*
* Find difference between point of intersection and center of disc.
*/
addscaledvec(*pos, dist, *ray, &hit);
vecsub(hit, disc->center, &hit);
/*
* If hit point is <= disc->radius from center, we've hit the disc.
*/
if (dotp(&hit, &hit) <= disc->radius)
return dist;
return 0.;
}
.D)
.NH 2
nrmdisc()
.LP
Each primitive normal routine is passed a location to a primitive, a pointer
to a point on the surface of the primitive, and a pointer to a vector
which must be set to the normal to the primitive at the point of intersection.
For the disc, this is very simple, as the disc is planar:
.D(
Vector
nrmdisc(pos, prim, nrm)
Vector *pos, *nrm;
Primitive *prim;
{
*nrm = prim->objpnt.p_disc->normal;
}
.D)
.NH 2
discextent()
.LP
The discextent() routine is passed a pointer to a disc Primitive as well
as a bounding
box stored as a 2 by 3 array of doubles. The routine computes the extent
of the disc along each axis and fills the bounding box array appropriately.
.LP
Note that the bounding box of all primitives should be at least
2*EPSILON along each axis to avoid problems with roundoff error.
Fortunately, primextent(),
routine which calls the primitive bounding box functions, will check
for and "widen" degenerate bounding boxes.
Thus, the
bounding box volumes are allowed to compute degenerate boxes.
Also, if a primitive is unbounded (e.g., a plane),
the maximum X extent should be set to be less than the minimum X extent.
.LP
So, discextent is written as:
.D(
discextent(prim, bounds)
Primitive *prim;
double bounds[2][3];
{
Disc *disc;
double extent, rad;
disc = prim->objpnt.p_disc;
rad = sqrt(disc->radius);
/*
* Project disc along each of X, Y and Z axes.
*/
extent = 1. - disc->normal.x * disc->normal.x;
extent *= rad;
bounds[LOW][X] = disc->center.x - extent;
bounds[HIGH][X] = disc->center.x + extent;
extent = 1. - disc->normal.y * disc->normal.y;
extent *= rad;
bounds[LOW][Y] = disc->center.y - extent;
bounds[HIGH][Y] = disc->center.y + extent;
extent = 1. - disc->normal.z * disc->normal.z;
extent *= rad;
bounds[LOW][Z] = disc->center.z - extent;
bounds[HIGH][Z] = disc->center.z + extent;
}
.D)
.NH
input_lex.l
.LP
Among other things, input_lex.l contains a list of all the keywords
recognized by rayshade. To this list we must add the keyword for
the new primitive type. Following the example of other keywords,
we add:
.D(
disc {return(tDISC);}
.D)
.NH
input_yacc.y
.LP
Near the top of input_yacc.y are the declarations for the tokens
returned by lex. We must add the new token, tDISC, to this list:
.D(
%token tBACKGROUND tBLOTCH tBOX tBUMP tCONE tCYL tDIRECTIONAL tDISC
.D)
Finally, we need to add the production to the yacc grammar which will
call makdisc(). We first modify the Primtype production to include
a new production named Disc:
.D(
Primtype : Plane
| Sphere
| Box
| Triangle
| Cylinder
| Cone
| Superq
| Poly
| HeightField
| Disc
;
.D)
.LP
Next, near the productions for the other primitives, we add:
.D(
Disc : tDISC tSTRING Vector Fnumber Vector
{
/*
* disc surfname center.x center.y center.z
* radius norm.x norm.y norm.z
*/
LastObj = maksph($2, $3, $4, $5);
}
;
.D)
.NH
Testing
.LP
Once you add "disc.o" to the OBJS list in the Makefile, you should be
able to re-compile rayshade. A good input file for testing the disc
primitive might be:
.D(
/*
* Demo picture of a textured ground plane with a sphere, cone
* and disc. The disc is situated on the top of the cone and faces
* up and towards the viewer.
*/
eyep 0. 25. 7.
screen 256 256
light 1.4 point -15. 20. 15.
surface red .02 0 0 .5 0 0 .2 .2 .2 32. 0.0 0 0
surface blacktile 0.01 0.015 0.01 0.02 0.03 0.02 0.3 0.35 0.3 30 0. 0 0
surface white .02 .02 .008 .5 .5 .25 0.8 0.8 0.8 18 0. 0 0
surface glass 0.02 0.02 0.02 0. 0. 0. 0.8 0.8 0.8 25 0. 0. 1.15
disc white -5. 3. 4. 3. 0. 1. 1.
sphere red 4. 3 0 0
/*
* Cone actually sticks through ground plane. This solves problems
* that arise when the bottom of the cone and the plane are coincident.
*/
cone glass -5. 3 -4.1 -5. 3. 4. 4. 0.
plane white 0. 0. 1. 0. 0. -4.
texture marble scale 4. 4. 4.
texture checker blacktile translate 0. 0. 0.3 scale 4. 4. 4.
.D)